<?php
/**
 * Created by PhpStorm.
 * User: Qch
 * Date: 2017/3/24
 * Time: 16:06
 */

namespace J\Util;


use J\Schema\Syntax;
use J\Exception;
use J\Message;
use J\Schema\Sprite;
use J\Util\Platform\Builder;

/**
 * 数据库操作类
 *   事务:  Transaction($call)
 *   CURD: execute($sql, $binding = [], $fetch_model = null)
 *
 *  fetch_model:
 *      -2: 不以ID为索引
 *      -1: insert
 *    null: update/delete
 *     int: select
 *
 */
class Database
{
    const FETCH_INSTALL = -1;
    const FETCH_CREATE  = -2;
    /**
     * @var \PDO
     */
    protected $pdo;

    /**
     * @var Builder
     */
    protected $builder;

    protected $configs = null;

    protected $id = -1;

    /**
     * 连接数据库
     *
     * @param string|[] $config 配置
     *
     * @throws Exception
     */
    protected function connect($config)
    {
        $config = is_string($config) ? dsn2config($config) : array_change_key_case($config);

        $this->builder = Builder::getInstance($config['type'], $this);
        $driverOptions = array_isset($config, 'driverOptions', []);

        $charset = 'SET NAMES ' .array_isset($config, 'charset', 'utf8');

        $driverOptions[\PDO::ATTR_ERRMODE] = \PDO::ERRMODE_EXCEPTION;
        if (version_compare(PHP_VERSION, '5.3.6', '<')) {
            //禁用模拟预处理语句
            $driverOptions[\PDO::ATTR_EMULATE_PREPARES] = false;
            // PHP5.3.6以下不支持charset设置
            $driverOptions[\PDO::MYSQL_ATTR_INIT_COMMAND] = $charset ;
            unset($config['charset']);
        }

        $dsn = $this->builder->buildDsn($config);
        try{
            $this->onSql($dsn);
            $this->pdo = new \PDO($dsn, $config['user'], $config['pass'], $driverOptions);
            $this->pdo->query($charset);

        }catch (\Throwable $e){
            throw new Exception(Message::CONNECT_ERROR, ['dsn'=>$dsn]);
        }
    }

    /**
     * 切换数据库
     * @param int $id 数据库标识
     * @throws Exception
     */
    public function reverse($id = 0)
    {
        if($this->id === $id) return;

        $config = array_isset($this->configs,$id,[]);
        if(empty($config)) throw new Exception('数据库未安装');

        $this->connect($config);
    }

    /**
     * 设置参数
     * @param array $config
     * @return void
     */
    public function setConfig($config)
    {
        $this->configs = $config;
    }

    /**
     * 事物回调操作
     *
     * @param \Closure $call 事务逻辑
     *
     * @return bool
     */
    public function Transaction($call)
    {
        $pdo = null;
        try{
            $pdo = $this->getPdo();
            $pdo->beginTransaction();
            $call($pdo);
            $pdo->commit();
            return true;
        }catch (\Throwable $e){
            $pdo and $pdo->rollBack();
            return false;
        }
    }

    /**
     * 运行SQL语句
     *
     * @param string $sql
     * @param Sprite[] $binding
     * @param null|int $fetch_model
     *
     * @return array|bool|int|string
     * @throws Exception
     */
    public function execute($sql, $binding = [], $fetch_model = null)
    {
//        try {
            $this->onSql($sql,$binding);

            $stat = $this->getPdo()->prepare($sql);

            foreach ($binding as $name => $value) {
                $stat->bindValue($name, $value->getValue(), $value->getBinding());
            }

            $r = $stat->execute();

            if(!$r) return $r;

            if($fetch_model === self::FETCH_INSTALL) {
                $r = $this->getPdo()->lastInsertId();
            }elseif($fetch_model === self::FETCH_CREATE){
                // 直接返回$r;
            }elseif ($fetch_model !== null) {
                $r =  $stat->fetchAll($fetch_model);
            } else {
                $r = $stat->rowCount();
            }

            return $r;
//        }
//        catch (\Throwable $e) {
//            app()->error(Message::SQL_RUN_ERROR, ['sql' => $sql, 'info' => $e->getMessage()]);
//            return false;
//        }
    }

    /**
     * @return \PDO
     * @throws Exception
     */
    public function getPdo()
    {
        if(!$this->pdo) $this->reverse();
        if(!$this->pdo) throw new Exception('数据库未连接');

        return $this->pdo;
    }

    /**
     * 默认SQl日志记录
     * @param string $sql
     * @param Sprite[] $values
     */
    protected function onSql($sql, $values = [])
    {
        $v = [];
        // array_map
        foreach ($values as $name => $value) $v[$name] = $value->getValue();

        app()->debug(Message::SQL_LOG, ['sql' => $sql, 'values' => $v]);
    }

    /**
     * 删除记录
     *
     * @param string $table 表名
     * @param int|int[] $keys ids
     *
     * @return int|false
     * @throws Exception
     */
    public function delete($table, $keys)
    {
        // 初始化数据库，方便读写切换
        $this->reverse();

        $sql = 'DELETE FROM ' . $table . ' WHERE ' . $this->prepareIds($keys);
        return $this->execute($sql);
    }

    /**
     * 增加一条记录
     *
     * @param string $table 表名
     * @param Sprite[] $value
     *
     * @return int|false
     * @throws Exception
     */
    public function add($table, $value)
    {
        // 初始化数据库，方便读写切换
        $this->reverse();

        list($list, $bind) = $this->builder->mapCalculator($value, false);
        $sql = 'INSERT INTO ' . $table
            . '(' . $list[0] . ')VALUES(' . $list[1] . ')';

        return $this->execute($sql, $bind, self::FETCH_INSTALL);
    }

    /**
     * 修改记录
     *
     * @param string $table 表名
     * @param int[]|int $keys
     * @param Sprite[] $exp
     *
     * @return int|false
     * @throws Exception
     */
    public function modify($table, $exp, $keys = null)
    {
        // 初始化数据库，方便读写切换
        $this->reverse();

        list($list, $bind) = $this->builder->mapCalculator($exp);

        $sql = 'UPDATE ' . $table . ' SET ' . $list . ' WHERE ' . $this->prepareIds($keys);

        return $this->execute($sql, $bind);
    }

    /**
     * 数据库操作
     * @param $sql
     * @throws Exception
     */
    public function create($sql)
    {
        $this->reverse();
        $this->execute($sql,[],self::FETCH_CREATE);
    }

    /**
     * 查询
     *
     * @param string $table
     * @param string[] $columns
     * @param array $conditions
     * @param array $option
     * @param int $fetch_model
     *
     * @return array|false
     * @throws Exception
     */
    public function select($table, $columns, $conditions, $option = null, $fetch_model = null)
    {
        if ($fetch_model === null) $fetch_model = \PDO::FETCH_UNIQUE | \PDO::FETCH_ASSOC;

        // 默认以ID索引
        if($fetch_model !== \PDO::FETCH_COLUMN) array_unshift($columns,'id');

        return $this->query($table, $columns, $conditions, $option, $fetch_model);
    }

    /**
     * 根据ID查询
     * @param $table
     * @param array|'*' $columns
     * @param $keys
     * @return array|false
     * @throws Exception
     */
    public function selectById($table, $columns, $keys)
    {
        // 初始化数据库，方便读写切换
        $this->reverse();

        // 不查询
        if (empty($columns)) return false;

        if ($columns !== '*') {
            array_unshift($columns, Syntax::KEY_ID, Syntax::KEY_ID);
            $columns = implode(',', $columns);
        }

        $sql = "SELECT $columns FROM  $table  WHERE " . $this->prepareIds($keys);

        return $this->execute($sql,[],\PDO::FETCH_UNIQUE | \PDO::FETCH_ASSOC);
    }

    /**
     * 获取ID序列
     * @param $table
     * @param $conditions
     * @param array $options
     * @return array|false
     * @throws Exception
     */
    public function getIds($table, $conditions, $options = [])
    {
        $ids = $this->query($table, Syntax::KEY_ID, $conditions, $options, \PDO::FETCH_COLUMN);
        // var_dump($ids);
        return $ids;
    }

    /**
     * 查询
     * @param $table
     * @param $columns
     * @param $conditions
     * @param array $option
     * @param null $fetch_model
     * @return array|false
     * @throws Exception
     */
    public function query($table, $columns, $conditions, $option = [], $fetch_model = null)
    {
        // 初始化数据库，方便读写切换
        $this->reverse();

        if($fetch_model === null) $fetch_model = \PDO::FETCH_ASSOC;

        if(is_array($columns)) $columns =  implode(',', $columns);

        list($condition, $binding) = $this->builder->mapCondition($conditions);

        if(empty($option)){
            $option = '';
        }else{
            list($group, $order, $limit) = $this->builder->mapOption($option);
            $option = "$group $order $limit";
        }

        $sql = "SELECT $columns FROM {$table} WHERE $condition $option";

        return $this->execute($sql, $binding, $fetch_model);
    }

    /**
     * @param $key
     * @return string
     * @throws Exception
     */
    protected function prepareIds($key)
    {
        if($key === null) return ' 0 ';
        if(is_bool($key)) return $key ? ' 1 ': ' 0 ';
        if(is_int($key)) return 'id = ' . $key;
        if (!is_list($key)) throw new Exception('ID list Error');
        if(count($key) === 1) return 'id = ' . $key[0];
        return $this->builder->createIn('id', $key);
    }
}